解析React Hooks中的典型闭包问题
什么是闭包,可以看看我这篇文章
# 经典闭包问题
下面这段代码会控制台每过三秒就会输出 0,就算已经执行setCount(count + 1)
。
function Example() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
console.log('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>count + 1</button>
<button onClick={handleAlertClick}>print count</button>
</div>
);
}
下面这段代码每过两秒就输出 0
function WatchCount() {
const [count, setCount] = useState(0);
useEffect(function () {
setInterval(function log() {
console.log(`Count is: ${count}`);
}, 2000);
}, []);
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>加1</button>
</div>
);
}
# 对于闭包问题解决的方法有几种:
前两种方式解决闭包引用问题,后两种解决旧 state 更新失败问题。
记得在 useEffect, useMemo, useCallback 的第二个参数中添加依赖项。如果 useEffect 有副作用的话,记得在 return 函数中清除副作用。官方也在 eslint 中添加对应的提示,如果在上面的 hooks 中没有添加对应的依赖,就会有提示。
function WatchCount() { const [count, setCount] = useState(0); useEffect( function () { const id = setInterval(function log() { console.log(`Count is: ${count}`); }, 2000); return () => { clearInterval(id); // 记得清除副作用 }; }, [count] ); // 重点 return ( <div> {count} <button onClick={() => setCount(count + 1)}>加1</button> </div> ); }
使用 Ref,每次 useRef 只会创建一次,ref.current 保存的值会实时更新
function WatchCount() { const [count, setCount] = useState(0); const ref = useRef(0); ref.current = count; // 重点 useEffect(function () { const id = setInterval(function log() { console.log(`Count is: ${ref.current}`); }, 2000); }, []); // 重点 return ( <div> {count} <button onClick={() => setCount(count + 1)}>加1</button> </div> ); }
以函数的形式更新 state。如果更新值的时候依赖于旧的 state 话,就要以函数的形式去更新 state,函数的传参就上次更新的 state,否则就会陷入闭包陷阱,每次更新的值都是一样的。
setCount((curCount) => curCount + 1);
使用 useReducer, 通过一个简易的 Redux 来更新 state,这种方式最为简洁,方便,比较推荐这种方式。当然要视情况而决定。
function reducer(count, action) { switch (action.type) { case 'add': return count + action.gap; default: return count; } } function WatchCount() { const [count, dispatch] = useReducer(reducer, 0); useEffect(function () { setInterval(function log() { dispatch({ type: 'add', gap: 1 }); }, 2000); }, []); return <div>{count}</div>; }
# 底层原因
现在看 hooks 所针对的 FunctionComponnet
。
无论开发者怎么折腾,一个对象都只能有一个 state
属性或者 memoizedState
属性,可是,谁知道可爱的开发者们会在 FunctionComponent
里写上多少个 useState
,useEffect
等等呢?
所以,react 用了链表这种数据结构来存储 FunctionComponent
里面的 hooks。
function App() {
const [count, setCount] = useState(1);
const [name, setName] = useState('howard');
useEffect(() => {}, []);
const text = useMemo(() => {
return 'ddd';
}, []);
}
这个对象的memoizedState
属性就是用来存储组件上一次更新后的 state
,next
毫无疑问是指向下一个 hook 对象。
在组件更新的过程中,hooks 函数执行的顺序是不变的,就可以根据这个链表拿到当前 hooks 对应的Hook
对象,函数式组件就是这样拥有了 state 的能力。
当前,具体的实现肯定比这三言两语复杂很多。
所以,知道为什么不能将 hooks 写到 if else 语句中了吧。因为这样可能会导致顺序错乱,导致当前 hooks 拿到的不是自己对应的 Hook 对象。
# Dan 老哥关于闭包问题的分析
- 函数式组件在每一次渲染都有它自己的…所有, 你可以想象成每次
render
的时候都形成了一次快照, 保存了所有下面的东西, 每一份快照都是不同且独立的。
即:- 每一次渲染都有自己的 props 和 state
- 每一次渲染都有自己的事件处理函数
- 每一次渲染都有自己的
useEffect()
- class 组件之所以有时候"不太对"的原因是, React 修改了 class 中的
this.state
使其指向永远最新状态